﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using bldrdb;
using System.Reflection;

namespace ActiveRecord
{

    /*
     http://en.wikipedia.org/wiki/Active_record_pattern
      
     DbMapper<> этот generic который создает соотвествие 
     [1 instance класса] = [1 запись в таблице]
     Поддерживаются только: int,int64, bool, string, DateTime, double.
     
     Для класса становятся доступны следующие команды: 
      CreateTable, Select Where, Insert, Update, Delete, Left Join
     
     Используются следующие соглашения:
     1)Имя таблицы равно имени класса
     2)Имя поля таблицы равно имени мембера класса
     3)По умолчанию, для связи master-detail используются поля Id и ParentId
               Пример:
     class MyClass
     {
        int Id; 
     }
     var dbtable=new DbMapper<MyClass>();
     var rows=dbtable.Where( r=>r.Id==1 );
     Это эквивалент SQL:
     select * from MyClass where Id=1
     class MyChild
     {
        int ParentId; 
        int F1;
     }
     var childf1=dbtable.Join<MyChild>.All()[0].Detail.F1;
     Это условный эквивалент SQL:
     select top 1 MyChild.F1 from MyClass left join on MyChild.ParentId=MyClass.Id
     Сортировки можно делать с помощью Linq уже после чтения данных из БД:
         var rows = detail.All();
         var sorted=(from c in rows 
                       orderby c.f2 descending 
                       select c).ToList();
     */

    ///<summary>
    ///Generic, который создает соотвествие 
    ///[1 instance класса] = [1 запись в таблице]
    ///</summary>
    ///<typeparam name="T">Класс для которого создается связь с БД</typeparam>
   
    class DbMapper<T> where T : new()
    {
        /// <summary>
        /// Прочесть все строки из таблицы.
        /// </summary>
        /// <returns>Список всех строк</returns>
        public List<T> All()
        {
            var res = new List<T>();
            using (var sp = proc("SELECT * FROM " + sql_table(), null))
            {
                var rs = sp.OpenResultSet();
                res = SqlConstructor<T>.read(rs);
            }
            return res;
        }
        /// <summary>
        /// Прочесть все данные из открытого IResultSet.
        /// Используется для работы с плоским SQL.
        /// </summary>
        /// <param name="resultSet">Открытый ренее IResultSet</param>
        /// <returns>Список всех строк</returns>
        
        public List<T> Fetch(IResultSet resultSet)
        {

            var res = SqlConstructor<T>.read(resultSet);
            return res;
        }
        /// <summary>
        /// Прочесть первую сроку таблицы, которая удовлетворяет заданному условию.
        /// Условие выражается лямбда функцией. Функция должны возврщать bool.
        /// </summary>
        public T First(Expression<Func<T, bool>> lambda)
        {
            var res = Where(lambda);
            if (res.Count > 0)
                return res[0];
            return default(T);
        }
        /// <summary>
        /// Прочесть строки которые удовлетворяют заданному условию.
        /// Условие выражается лямбда функцией. Функция должны возврщать bool.
        /// </summary>
        public List<T> Where(Expression<Func<T, bool>> lambda)
        {
            var sqlparams = new Dictionary<string, string>();
            string wheresql = SqlConstructor<T>.compileWhere(lambda, sqlparams);
            
            using (var sp = proc("SELECT * FROM " + sql_table() + wheresql, sqlparams))
            {
                var rs = sp.OpenResultSet();
                return SqlConstructor<T>.read(rs);
            }
            
        }
        /// <summary>
        /// Добавить в таблицу строку.
        /// </summary>
        public void Insert(T rec)
        {
            var sqlparams = new Dictionary<string, string>();
            string insertsql = SqlConstructor<T>.SQLInsert(rec, sqlparams);
            procExec(insertsql, sqlparams);
        }
        string sql_table()
        {
            return typeof(T).Name;
        }
        /// <summary>
        /// Создать таблицу.
        /// Опционально, в классе по которому создается таблица,
        /// можно использовать аттрибуты DDL и DDL_CONSTRAINT.
        /// </summary>
        public void CreateTable()
        {

            using (var sp = proc("", null))
            {

                if (SqlConstructor<T>.IsTableExists(sp))
                {
                    sp.SetSQL("DROP TABLE " + sql_table());
                    sp.Execute();
                }

                string createSQL = SqlConstructor<T>.SQLCreateTable();
                sp.SetSQL(createSQL);
                sp.Execute();

            }

        }
        /// <summary>
        /// Изменить строки в таблице.
        /// Изменяются только те поля, коорые указаны в инстансе upd_object анонимного класса 
        /// Условие выборки строк задается лямбдой
        /// </summary>
        public void Update(object upd_object, Expression<Func<T, bool>> lambda)
        {
            var parameters = new Dictionary<string, string>();
            var wheresql = SqlConstructor<T>.compileWhere(lambda, parameters);
            var updatesql = SqlConstructor<T>.SQLUpdate(upd_object);
            procExec(updatesql + wheresql, parameters);
        }
        /// <summary>
        /// Изменить строки в таблице.
        /// Изменяются все поля.
        /// Условие выборки строк задается лямбдой
        /// </summary>
        public void Update(T trec, Expression<Func<T, bool>> lambda)
        {
            var parameters = new Dictionary<string, string>();
            var wheresql = SqlConstructor<T>.compileWhere(lambda, parameters);
            var updatesql = SqlConstructor<T>.SQLUpdate(typeof(T), trec);
            procExec(updatesql + wheresql, parameters);
        }
        /// <summary>
        /// Удалить строки в таблице.
        /// Условие выборки строк задается лямбдой
        /// </summary>
        public void Delete(Expression<Func<T, bool>> lambda)
        {
            var parameters = new Dictionary<string, string>();
            var wheresql = SqlConstructor<T>.compileWhere(lambda, parameters);
            procExec("DELETE FROM " + sql_table() + wheresql, parameters);
        }


        public JoinedTables<T, C> Join<C>()
            where C : new()
        {
            JoinedTables<T, C> res = new JoinedTables<T, C>();
            return res;
        }
        public JoinedTables<T, C> Join<C>(Expression<Func<T, C, bool>> lambda)
                where C : new()
        {
            var res = new JoinedTables<T, C>(lambda);
            return res;
        }

        void procExec(string sql, Dictionary<string, string> parameters)
        {
            using (var sp = proc(sql, parameters))
            {
                sp.Execute();
            }
        }
        IStorProc proc(string sql, Dictionary<string, string> parameters)
        {
            IStorProc sp = bldrdb.DBFactory.Create();
            sp.SetSQL(sql);
            if (parameters != null)
            {
                foreach (var p in parameters)
                    sp.AddParam(p.Key, p.Value);
            }
            return sp;
        }
    }

    //Если поле класса содержит этот атрибут,
    //то для дефиниции поля используется атрибут а не автосгенеренноое значение
    //class t5
    //{
    //    [DDL("f1 int NOT NULL")]
    //    public int f1;

    [AttributeUsage(
     AttributeTargets.Field |
     AttributeTargets.Property,
     AllowMultiple = false)]
    public class DDL : System.Attribute
    {
        public string FieldPattern;
        public DDL(string fieldpattern)
        {
            FieldPattern = fieldpattern;
        }
    }

    [AttributeUsage(AttributeTargets.Class ,AllowMultiple = false)]
    public class DDL_CONSTRAINT : System.Attribute
    {
        public string FieldPattern;
        public DDL_CONSTRAINT(string fieldpattern)
        {
            FieldPattern = fieldpattern;
        }
    }
    
    public class JoinRecord<F, S>
    {
        public F Master;
        public S Detail;
    }

    class JoinedTables<F, S> where F : new()
        where S : new()
                            
    {
        string onsql;
        public JoinedTables(Expression<Func<F,S, bool>> lambda)
        {
            Expression<Func<F, S, bool>> joinexpression;
            joinexpression = lambda;

            var fake = new Dictionary<string, string>();
            onsql = SqlConstructor.processExpression(joinexpression,fake);
        }
        public JoinedTables()
        {
            //у T должен быть поле Id
            //у С должен быть поле ParentId
            Type t1 = typeof(F);
            var field_Id=t1.GetField("Id");
            if (field_Id == null)
                throw new Exception("Type '"+t1.Name+"' should has 'Id' field");

            Type t2 = typeof(S);
            var field_Id2 = t2.GetField("ParentId");
            if (field_Id2 == null)
                throw new Exception("Type '" + t2.Name + "' should has 'ParentId' field");

            onsql = t2.Name + ".ParentId=" + t1.Name + ".Id";
        }


        IStorProc proc()
        {
            IStorProc sp = bldrdb.DBFactory.Create();
            return sp;
        }
        public List<JoinRecord<F, S>> Where(Expression<Func<F, S,bool>> lambda)
        {
            var sqlparams=new Dictionary<string, string>();
            var res = new List<JoinRecord<F, S>>();
            string t1 = typeof(F).Name;
            string t2 = typeof(S).Name;
            string wheresql =" WHERE "+ SqlConstructor.processExpression(lambda, sqlparams);
            
            string joinsql = "LEFT JOIN "+t2+" ON  " + onsql;
            string detail_fields = SqlConstructor<S>.ForEachField(f => t2 + "." + f.Name +" as "+t2+"_"+f.Name);
            string sql = "SELECT "+t1+".*, "+detail_fields+" FROM "+t1+" " + joinsql + wheresql;
            
            using (var sp = proc())
            {
                sp.SetSQL(sql);

                foreach (var p in sqlparams)
                    sp.AddParam(p.Key, p.Value);

                var rs = sp.OpenResultSet();
                while (rs.Read())
                {
                    var rec = new JoinRecord<F, S>();

                    F f = new F();
                    SqlConstructor<F>.readonerec(f, rs,"");
                    S s = new S();
                    SqlConstructor<S>.readonerec(s, rs,t2+"_");

                    rec.Master = f;
                    rec.Detail = s;

                    res.Add(rec);
                }
            }
            return res;
        }
        

    }
    

    class SqlConstructor
    {

        public static string processExpression(LambdaExpression  lambda,
             Dictionary<string, string> sqlparams)
        {
            return processNode(lambda.Body, sqlparams);
        }
        static Dictionary<ExpressionType, string> binExpressionMappings = new Dictionary<ExpressionType, string>() 
        { 
        {ExpressionType.Equal,"{0}={1}"},
        {ExpressionType.GreaterThan,"{0}>{1}"},
        {ExpressionType.GreaterThanOrEqual,"{0}>={1}"},
        {ExpressionType.LessThan,"{0}<{1}"},
        {ExpressionType.LessThanOrEqual,"{0}<={1}"},
        {ExpressionType.NotEqual,"{0}<>{1}"},
        {ExpressionType.AndAlso,"({0}) AND ({1})"},
        {ExpressionType.OrElse,"({0}) OR ({1})"}
        
        };
        public static string processNode(Expression expr, Dictionary<string, string> sqlparams)
        {

            string clause = "";

            var be = expr as System.Linq.Expressions.BinaryExpression;
            if (be != null)
            {
                string bLeft = processNode(be.Left, sqlparams);
                string bRight = processNode(be.Right, sqlparams);
                string pattern="";
                if (binExpressionMappings.TryGetValue(expr.NodeType, out pattern))
                {
                    clause = string.Format(pattern, bLeft, bRight);
                    return clause;
                }

            }

            switch (expr.NodeType)
            {
                case ExpressionType.MemberAccess:
                    var me = (MemberExpression)expr;
                    string tablename=me.Member.DeclaringType.Name;
                    clause = tablename+"."+me.Member.Name;
                    break;

                case ExpressionType.Constant:
                    var ce = (ConstantExpression)expr;
                    string pname="p" + sqlparams.Count;
                    sqlparams.Add(pname, ce.ToString());
                    clause = "@" + pname;
                    break;
                default:
                    throw new Exception("Unsupported expression node:" + expr.NodeType.ToString());
            };

            return clause;
        }
    }

    
    class SqlConstructor<T> where T : new()
    {

        public static string compileWhere(Expression<Func<T, bool>> lambda,
            Dictionary<string, string> sqlparams)
        {
            return " WHERE "+SqlConstructor.processExpression(lambda,sqlparams);
           
        }
        public static bool IsTableExists(IStorProc sp)
        {
            string query = @"SELECT name FROM sys.Tables where name= '" + sql_table() + "';";
            sp.SetSQL(query);
            sp.Execute();
            IResultSet rs = sp.OpenResultSet();
            bool res = rs.Read();
            rs.Close();
            return res;

        }
        public static string sql_table()
        {
            return typeof(T).Name;
        }
        public static string field_process(FieldInfo f)
        {
            var attributes =f.GetCustomAttributes(typeof(DDL), false);
            if (attributes.Length > 0)
            {
                return ((DDL)attributes[0]).FieldPattern;
            }
            return "\r\n" + f.Name + " " + clrTypeToSqlType(f.FieldType);
        }
        public static string SQLCreateTable()
        {
            string sql = "CREATE TABLE " + sql_table() + " (";

            string fdefs = ForEachField(field_process);
            sql += fdefs;
            
            var attributes = typeof(T).GetCustomAttributes(typeof(DDL_CONSTRAINT), false);
            if (attributes.Length > 0)
            {
                sql +=",\r\n"+ ((DDL_CONSTRAINT)attributes[0]).FieldPattern;
            }
            

            sql += "\r\n);";
            return sql;
        }

        static Dictionary<Type, string> typemapping = new Dictionary<Type, string> () 
        { 
        { typeof(int), "int" }, { typeof(bool), "bit" }, { typeof(float), "float" }, 
        { typeof(double), "float" }, { typeof(byte), "int" }, { typeof(DateTime), "datetime" },
        { typeof(long), "bigint" }
        };
        static string clrTypeToSqlType(Type t)
        {
            string sql ="" ;
            if (typemapping.TryGetValue(t, out sql))
            {
                return sql;
            }
            return "nvarchar(max)";
        }
 
  
        static public string SQLUpdate(object rec)
        {

            string names = "";

            PropertyInfo[] piList = rec.GetType().GetProperties();
            int i = 0;
            foreach (PropertyInfo f in piList)
            {

                names += f.Name + "=" + "'" + f.GetValue(rec, null) + "'";
                if (i < (piList.Length - 1))
                {
                    names += ", ";
                }
                i++;
            }

            return string.Format("UPDATE {0} SET {1}", sql_table(), names);
        }
        static public string SQLUpdate(Type t, object rec)
        {
            var names=ForEachField(f => f.Name + "=" + "'" + f.GetValue(rec) + "'");
            return string.Format("UPDATE {0} SET {1}", sql_table(), names);
        }
        static public string ForEachField(Func<FieldInfo,string> lambda)
        {
            string names = "";
            Type t = typeof(T);
            var fields = t.GetFields();
            int i = 0;
            foreach (var f in fields)
            {
                names += lambda.Invoke(f);
       
                if (i < (fields.Length - 1))
                {
                    names += ", ";
                }

                i++;
            }
            return names;
        }
    
        static public string SQLInsert(T rec,Dictionary<string, string> sqlparams)
        {
            string names = ForEachField(f => f.Name);
            string values = ForEachField(delegate(FieldInfo f)
            {
                string pname="p" + sqlparams.Count;
                sqlparams.Add(pname, f.GetValue(rec).ToString());
                return "@" + pname;
            });

            return string.Format("INSERT INTO {0} ({1}) VALUES ({2})", sql_table(), names, values);
        }
        public static List<T> read(IResultSet rs)
        {
            List<T> res = new List<T>();
            while (rs.Read())
            {
                T rec = new T();
                readonerec(rec, rs,"");
                res.Add(rec);
            }
            return res;
        }
        static Dictionary<Type, Func<string, object>> parsers = new Dictionary<Type, Func<string, object>>()
        {
            {typeof(Int32), s=>Int32.Parse(s)},
            {typeof(DateTime), s=>DateTime.Parse(s)},
            {typeof(Int64), s=>Int64.Parse(s)},
            {typeof(Single), s=>Single.Parse(s)},
            {typeof(Double), s=>Double.Parse(s)},
            {typeof(Boolean), s=> (s=="1")}

        };
        public static void readonefield(FieldInfo f, string oval, T rec)
        {
            Func<string, object> parser;
            if (parsers.TryGetValue(f.FieldType, out parser))
            {
                f.SetValue(rec, parser.Invoke(oval));
            }
            else
                f.SetValue(rec, oval);
         
        }
        public static void readonerec(T rec, IResultSet sp, string field_prefix)
        {
            Type t = typeof(T);
            var fields = t.GetFields();
            foreach (var f in fields)
            {
                string val = sp[field_prefix+f.Name];
                readonefield(f, val, rec);
            }
        }

    }

}
